├── .nvmrc ├── .eslintignore ├── src ├── components │ ├── .gitkeep │ ├── editor │ │ ├── run-fab │ │ │ ├── utils.ts │ │ │ ├── actions.ts │ │ │ └── index.tsx │ │ ├── io-nav │ │ │ ├── output-fragment.tsx │ │ │ ├── input-fragment.tsx │ │ │ ├── index.tsx │ │ │ └── savelist.tsx │ │ ├── fontsize-picker.tsx │ │ ├── code-editor.tsx │ │ └── language-picker.tsx │ ├── base │ │ ├── toast.tsx │ │ └── modal.tsx │ ├── layout │ │ ├── banner.tsx │ │ ├── screen-size-modal.tsx │ │ └── navbar.tsx │ └── auth │ │ └── login-modal.tsx ├── store │ ├── action-types │ │ ├── auth.ts │ │ ├── savelist.ts │ │ ├── ui.ts │ │ └── editor.ts │ ├── getters │ │ ├── auth.ts │ │ ├── ui.ts │ │ ├── savelist.ts │ │ └── editor.ts │ ├── action │ │ ├── auth.ts │ │ ├── savelist.ts │ │ ├── ui.ts │ │ └── editor.ts │ ├── index.ts │ └── reducers │ │ ├── auth.ts │ │ ├── savelist.ts │ │ ├── ui.ts │ │ └── editor.ts ├── constants │ ├── panel.ts │ ├── modal.ts │ └── fontsizes.ts ├── utils │ ├── store.ts │ └── jwt.ts ├── index.tsx ├── config.ts ├── initializers │ ├── stubs.ts │ ├── languages.ts │ ├── index.ts │ ├── user.ts │ ├── settings.ts │ └── code.ts ├── app.tsx ├── services │ ├── settings.ts │ ├── api.ts │ ├── judge.ts │ ├── auth.ts │ └── ide.ts ├── data │ └── banner.json ├── tasks │ ├── savecodelist.ts │ └── save-update-code.ts ├── index.html ├── views │ └── main.tsx └── style │ └── app.scss ├── .prettierrc ├── tsconfig.json ├── .eslintrc.json ├── .github └── workflows │ └── main.yml ├── webpack.config.js ├── package.json └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 15 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/ -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/action-types/auth.ts: -------------------------------------------------------------------------------- 1 | export const LOGIN = 'auth/LOGIN'; 2 | export const LOGOUT = 'auth/LOGOUT'; 3 | -------------------------------------------------------------------------------- /src/constants/panel.ts: -------------------------------------------------------------------------------- 1 | export const IO_PANEL = 'io-panel'; 2 | export const SAVELIST_PANEL = 'savelist-panel'; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "tabWidth": 2 6 | } -------------------------------------------------------------------------------- /src/constants/modal.ts: -------------------------------------------------------------------------------- 1 | export const LOGIN_MODAL = 'login-modal'; 2 | export const SCREEN_SIZE_MODAL = 'screen-size-modal'; 3 | -------------------------------------------------------------------------------- /src/components/editor/run-fab/utils.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (time: number) => new Promise((resolve) => setTimeout(resolve, time)); 2 | -------------------------------------------------------------------------------- /src/constants/fontsizes.ts: -------------------------------------------------------------------------------- 1 | export const FONTSIZE_MAP = { 2 | SMALL: '12px', 3 | MEDIUM: '14px', 4 | LARGE: '16px', 5 | XL: '18Px', 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/store.ts: -------------------------------------------------------------------------------- 1 | export const listToMap = (arr, key = 'id', mapper = (obj) => obj) => 2 | arr.reduce((acc, curr) => { 3 | acc[curr[key]] = mapper(curr); 4 | return acc; 5 | }, {}); 6 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from '~/app'; 4 | import '~/style/app.scss'; 5 | 6 | ReactDOM.render(, document.querySelector('#root')); 7 | -------------------------------------------------------------------------------- /src/store/getters/auth.ts: -------------------------------------------------------------------------------- 1 | export const getUser = () => (state) => state.auth.user; 2 | export const getIsLoggedIn = () => (state) => state.auth.isLoggedIn; 3 | export const getJwt = () => (state) => state.auth.jwt; 4 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | API: { 3 | HOST: process.env.API_HOST || 'http://localhost:9000', 4 | }, 5 | GOOGLE_CLIENT_ID: process.env.REACT_APP_GOOGLE_CLIENT_ID, 6 | GITHUB_CLIENT_ID: process.env.REACT_APP_GITHUB_CLIENT_ID, 7 | }; 8 | -------------------------------------------------------------------------------- /src/store/action-types/savelist.ts: -------------------------------------------------------------------------------- 1 | export const SET_SAVELIST = 'savelist/SET_SAVELIST'; 2 | export const SET_CURRENT_PAGE = 'savelist/SET_CURRENT_PAGE'; 3 | export const CLEAR_SAVELIST = 'savelist/CLEAR_SAVELIST'; 4 | export const SET_PAGE_METADATA = 'savelist/SET_PAGE_METADATA'; 5 | -------------------------------------------------------------------------------- /src/utils/jwt.ts: -------------------------------------------------------------------------------- 1 | const TOKEN = 'coding-minutes-auth-jwt'; 2 | 3 | export function getJwt() { 4 | return localStorage.getItem(TOKEN); 5 | } 6 | 7 | export function setJwt(jwt: string) { 8 | localStorage.setItem(TOKEN, jwt); 9 | } 10 | 11 | export function clearJwt() { 12 | localStorage.removeItem(TOKEN); 13 | } 14 | -------------------------------------------------------------------------------- /src/store/action/auth.ts: -------------------------------------------------------------------------------- 1 | import { LOGIN, LOGOUT } from '../action-types/auth'; 2 | import { AuthState } from '../reducers/auth'; 3 | 4 | export const loginUser = (auth: AuthState) => ({ 5 | type: LOGIN, 6 | payload: auth, 7 | }); 8 | 9 | export const logoutUser = () => ({ 10 | type: LOGOUT, 11 | payload: null, 12 | }); 13 | -------------------------------------------------------------------------------- /src/initializers/stubs.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import { getStubs } from '~/services/judge'; 3 | import { setCodeStubs } from '~/store/action/editor'; 4 | 5 | export const loadStubs = async (dispatch: Dispatch) => { 6 | const response: any = await getStubs(); 7 | 8 | dispatch(setCodeStubs(response.data.data)); 9 | }; 10 | -------------------------------------------------------------------------------- /src/store/action-types/ui.ts: -------------------------------------------------------------------------------- 1 | export const TOGGLE_IO_PANE = 'ui/TOGGLE_IO_PANE'; 2 | export const SET_ACTIVE_MODAL = 'ui/SET_ACTIVE_MODAL'; 3 | export const TOGGLE_BANNER = 'ui/TOGGLE_BANNER'; 4 | export const TOGGLE_OPTIONS_MENU = 'ui/TOGGLE_OPTIONS_MENU'; 5 | export const SET_ACTIVE_PANEL = 'ui/SET_ACTIVE_PANEL'; 6 | export const SET_TOAST = 'ui/SET_TOAST'; 7 | -------------------------------------------------------------------------------- /src/initializers/languages.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import { getLanguages } from '~/services/judge'; 3 | import { setLanguages } from '~/store/action/editor'; 4 | 5 | export const loadLanguages = async (dispatch: Dispatch) => { 6 | const response: any = await getLanguages(); 7 | 8 | dispatch(setLanguages(response.data.data)); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/editor/run-fab/actions.ts: -------------------------------------------------------------------------------- 1 | import { getSubmission } from '~/services/judge'; 2 | 3 | export interface Submission { 4 | time: string; 5 | memory: number; 6 | stdout: string; 7 | stderr: string; 8 | compile_output: string; 9 | } 10 | 11 | export const pollSubmission = (submissionId: string): Promise => 12 | getSubmission(submissionId).then((response) => response.data.data); 13 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { MainView } from '~/views/main'; 5 | import { initStore } from '~/store'; 6 | import { BrowserRouter as Router } from 'react-router-dom'; 7 | 8 | const store = initStore(); 9 | 10 | export const App: React.FC = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/store/getters/ui.ts: -------------------------------------------------------------------------------- 1 | export const isIOPaneOpen = () => (state) => state.ui.io_pane_open; 2 | export const getActiveModalName = () => (state) => state.ui.active_modal; 3 | export const isBannerVisible = () => (state) => state.ui.isBannerVisible; 4 | export const isOptionsMenuOpen = () => (state) => state.ui.isOptionsMenuOpen; 5 | export const getActivePanel = () => (state) => state.ui.active_panel; 6 | export const getToast = () => (state) => state.ui.toast; 7 | -------------------------------------------------------------------------------- /src/store/getters/savelist.ts: -------------------------------------------------------------------------------- 1 | export const getCurrentPageNumber = () => (state) => state.savelist.page; 2 | export const getCurrentPageList = () => (state) => state.savelist.codes; 3 | export const getNextPageNumber = () => (state) => state.savelist.next; 4 | export const getPreviousPageNumber = () => (state) => state.savelist.previous; 5 | export const getTotalPages = () => (state) => state.savelist.total_pages; 6 | export const getCodesCount = () => (state) => state.savelist.count; 7 | -------------------------------------------------------------------------------- /src/services/settings.ts: -------------------------------------------------------------------------------- 1 | const STORAGE_KEY = 'coding-minutes-ide-user-settings'; 2 | 3 | export function getSettings() { 4 | const settings = localStorage.getItem(STORAGE_KEY); 5 | if (settings) return JSON.parse(settings); 6 | return {}; 7 | } 8 | 9 | export function setSettings(settings) { 10 | const oldSettings = getSettings(); 11 | const newSettings = { 12 | ...oldSettings, 13 | ...settings, 14 | }; 15 | localStorage.setItem(STORAGE_KEY, JSON.stringify(newSettings)); 16 | } 17 | -------------------------------------------------------------------------------- /src/initializers/index.ts: -------------------------------------------------------------------------------- 1 | import { loadStubs } from './stubs'; 2 | import { getUserFromJwt } from './user'; 3 | import { fetchCodeFromIdParam } from './code'; 4 | import { loadLanguages } from './languages'; 5 | import { loadSettings } from './settings'; 6 | 7 | export const initialize = function* (dispatch) { 8 | yield getUserFromJwt(dispatch); 9 | yield loadLanguages(dispatch); 10 | yield loadStubs(dispatch); 11 | yield loadSettings(dispatch); 12 | yield fetchCodeFromIdParam(dispatch); 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/editor/io-nav/output-fragment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { getStdout } from '~/store/getters/editor'; 4 | 5 | export const OutputFragment: React.FC = () => { 6 | const stdoutB64 = useSelector(getStdout()); 7 | const stdout = stdoutB64 && atob(stdoutB64); 8 | 9 | return ( 10 |
11 |
Output
12 |
{stdout}
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/store/action-types/editor.ts: -------------------------------------------------------------------------------- 1 | export const SET_SOURCE = 'editor/SET_SOURCE'; 2 | export const SET_STDOUT = 'editor/SET_STDOUT'; 3 | export const SET_STDIN = 'editor/SET_STDIN'; 4 | export const SET_RETURN_CODE = 'editor/SET_RETURN_CODE'; 5 | export const SET_STUBS = 'editor/SET_STUBS'; 6 | export const SET_LANGUAGES = 'editor/SET_LANGUAGES'; 7 | export const SET_SELECTED_LANGUAGE_BY_ID = 'editor/SET_SELECTED_LANGUAGE_BY_ID'; 8 | export const SET_FONT_SIZE = 'editor/SET_FONT_SIZE'; 9 | export const SET_FILENAME = 'editor/SET_FILENAME'; 10 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Config from '~/config'; 3 | import { getJwt } from '~/utils/jwt'; 4 | 5 | const client = axios.create({ 6 | baseURL: Config.API.HOST + '/api', 7 | responseType: 'json', 8 | }); 9 | 10 | client.interceptors.request.use( 11 | (request) => { 12 | const token = getJwt(); 13 | if (token) request.headers['Authorization'] = `JWT ${token}`; 14 | return request; 15 | }, 16 | (error) => { 17 | console.error(error); 18 | }, 19 | ); 20 | 21 | export default client; 22 | -------------------------------------------------------------------------------- /src/services/judge.ts: -------------------------------------------------------------------------------- 1 | import Api from './api'; 2 | const BASE_URL = '/judge'; 3 | 4 | export async function getLanguages() { 5 | return Api.get(`${BASE_URL}/languages`); 6 | } 7 | 8 | export async function getSubmission(submissionId: string) { 9 | return Api.get(`${BASE_URL}/submissions/${submissionId}`); 10 | } 11 | 12 | export async function runCode(data: { source_code; language_id; stdin }) { 13 | return Api.post(`${BASE_URL}/run`, data); 14 | } 15 | 16 | export async function getStubs() { 17 | return Api.get(`${BASE_URL}/stubs`); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "lib": ["dom"], 5 | "target": "es2019", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "outDir": "./dist", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "baseUrl": "src", 15 | "rootDir": "src", 16 | "paths": { 17 | "~/*": ["*"] 18 | }, 19 | "resolveJsonModule": true 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /src/data/banner.json: -------------------------------------------------------------------------------- 1 | { 2 | "SANTA": { 3 | "content": "Start your new year with Coding, use code on all courses! 🎉", 4 | "link": "https://codingminutes.com/#course_gallery" 5 | }, 6 | "COMPETITIVE_PROGRAMMING_ESSENTIALS": { 7 | "content": "Kickstart Coding, enroll in for ", 8 | "link": "https://www.udemy.com/course/learn-coding-for-beginners/?referralCode=B9C51C703049157C1298" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/services/auth.ts: -------------------------------------------------------------------------------- 1 | import Api from './api'; 2 | const BASE_URL = '/auth'; 3 | 4 | export async function loginWithGoogle(token: string) { 5 | const response = await Api.post(`${BASE_URL}/login`, { token }); 6 | return response.data; 7 | } 8 | 9 | export async function loginWithGithub(code: string) { 10 | const response = await Api.post(`${BASE_URL}/login`, { code, strategy: "github" }) 11 | return response.data; 12 | } 13 | 14 | export async function verifyToken() { 15 | const response = await Api.post(`${BASE_URL}/verify`); 16 | return response; 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["plugin:react/recommended", "airbnb"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": ["react", "@typescript-eslint"], 16 | "rules": { 17 | "react/jsx-filename-extension": [1, { "extensions": [".tsx"] }], 18 | "no-use-before-define": "off", 19 | "@typescript-eslint/no-use-before-define": ["error"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/services/ide.ts: -------------------------------------------------------------------------------- 1 | import Api from './api'; 2 | import useSWR from 'swr'; 3 | const BASE_URL = '/code'; 4 | 5 | export function saveCode(data) { 6 | return Api.post(`${BASE_URL}/`, data); 7 | } 8 | 9 | export async function getCodeById(id: string) { 10 | return Api.get(`${BASE_URL}/${id}`); 11 | } 12 | 13 | export async function updateCode(id: string, data) { 14 | return Api.patch(`${BASE_URL}/${id}`, data); 15 | } 16 | 17 | export async function getSavedCodes(page: number = 1, query = '') { 18 | const { data } = useSWR(`${BASE_URL}/saved?page=${page}&query=${query}`, (url) => Api.get(url)); 19 | return data; 20 | } 21 | -------------------------------------------------------------------------------- /src/initializers/user.ts: -------------------------------------------------------------------------------- 1 | import { verifyToken } from '~/services/auth'; 2 | import { clearJwt, getJwt } from '~/utils/jwt'; 3 | import { Dispatch } from 'redux'; 4 | import { loginUser } from '~/store/action/auth'; 5 | 6 | export async function getUserFromJwt(dispatch: Dispatch) { 7 | try { 8 | const jwt = getJwt(); 9 | if (!jwt) return; 10 | 11 | const response = await verifyToken(); 12 | const user = response.data; 13 | 14 | dispatch( 15 | loginUser({ 16 | user, 17 | jwt, 18 | isLoggedIn: true, 19 | }), 20 | ); 21 | } catch (error) { 22 | console.error(error); 23 | clearJwt(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/tasks/savecodelist.ts: -------------------------------------------------------------------------------- 1 | import { getSavedCodes } from '~/services/ide'; 2 | import { Dispatch } from 'redux'; 3 | import { setCurrentPage, updateSavelist, setPageMetaData } from '~/store/action/savelist'; 4 | import { batch } from 'react-redux'; 5 | 6 | export async function fetchSavedCodes(dispatch: Dispatch, page: number = 1, query = '') { 7 | const response = await getSavedCodes(page, query); 8 | if (response?.data) { 9 | const data = response.data; 10 | 11 | batch(() => { 12 | dispatch(setCurrentPage(page)); 13 | dispatch(updateSavelist(data.data)); 14 | dispatch(setPageMetaData(data.count, data.next, data.previous, data.total_pages)); 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/initializers/settings.ts: -------------------------------------------------------------------------------- 1 | import { batch } from 'react-redux'; 2 | import { Dispatch } from 'redux'; 3 | import { IO_PANEL } from '~/constants/panel'; 4 | import { getSettings } from '~/services/settings'; 5 | import { setLanguageById, setFontSize } from '~/store/action/editor'; 6 | import { setActivePanel } from '~/store/action/ui'; 7 | 8 | export const loadSettings = (dispatch: Dispatch) => { 9 | const settings = getSettings(); 10 | batch(() => { 11 | if (settings?.lang_id) { 12 | dispatch(setLanguageById(settings.lang_id)); 13 | } 14 | if (settings?.fontSize) { 15 | dispatch(setFontSize(settings.fontSize)); 16 | } 17 | dispatch(setActivePanel(IO_PANEL)); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/store/getters/editor.ts: -------------------------------------------------------------------------------- 1 | export const getSelectedLanguage = () => (state) => state.editor.selectedLanguage; 2 | export const getCurrentSource = () => (state) => 3 | state.editor.sourceLanguageMap[state.editor.selectedLanguage?.id] || ' '; 4 | export const getStdin = () => (state) => state.editor.stdin; 5 | export const getStdout = () => (state) => state.editor.stdout; 6 | export const getReturnCode = () => (state) => state.editor.returnCode; 7 | export const getLanguages = () => (state) => Object.values(state.editor.languages); 8 | export const getFontSize = () => (state) => state.editor.fontSize; 9 | export const getLanguageMap = () => (state) => state.editor.languages; 10 | export const getFilename = () => (state) => state.editor.filename; 11 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | 4 | import { editorReducer } from '~/store/reducers/editor'; 5 | import { uiReducer } from '~/store/reducers/ui'; 6 | import { authReducer } from '~/store/reducers/auth'; 7 | import { savelistReducer } from '~/store/reducers/savelist'; 8 | 9 | export const initStore = () => { 10 | const rootReducer = combineReducers({ 11 | editor: editorReducer, 12 | ui: uiReducer, 13 | auth: authReducer, 14 | savelist: savelistReducer, 15 | }); 16 | const composeEnhancers = composeWithDevTools({}); 17 | const store = createStore(rootReducer, composeEnhancers()); 18 | 19 | return store; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/base/toast.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { setToast } from '~/store/action/ui'; 4 | import { getToast } from '~/store/getters/ui'; 5 | 6 | interface Props {} 7 | 8 | export const Toast: React.FC = (props) => { 9 | const toast = useSelector(getToast()); 10 | const dispatch = useDispatch(); 11 | 12 | React.useEffect(() => { 13 | if (toast) { 14 | setTimeout(() => { 15 | dispatch(setToast(null)); 16 | }, toast.timeout); 17 | } 18 | }, [toast]); 19 | 20 | // TODO: Add ToastType styling 21 | 22 | const message = toast ? toast.message : ''; 23 | 24 | return
{message}
; 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/editor/io-nav/input-fragment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { setStdin } from '~/store/action/editor'; 4 | import { getStdin } from '~/store/getters/editor'; 5 | 6 | export const InputFragment: React.FC = () => { 7 | const dispatch = useDispatch(); 8 | 9 | const stdin = useSelector(getStdin()); 10 | const setInput = (input) => dispatch(setStdin(input)); 11 | 12 | return ( 13 |
14 |
Enter Input
15 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/layout/banner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { toggleBanner } from '~/store/action/ui'; 3 | import { useDispatch } from 'react-redux'; 4 | 5 | interface Props { 6 | content: string; 7 | link: string; 8 | } 9 | 10 | export const Banner: React.FC = ({ link, content }) => { 11 | const dispatch = useDispatch(); 12 | 13 | function closeBanner() { 14 | dispatch(toggleBanner()); 15 | } 16 | 17 | return ( 18 | <> 19 |
20 |
21 | 22 |
23 | 24 |
25 | ✕ 26 |
27 |
28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/store/action/savelist.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CURRENT_PAGE, 3 | SET_SAVELIST, 4 | CLEAR_SAVELIST, 5 | SET_PAGE_METADATA, 6 | } from '../action-types/savelist'; 7 | import { Codefile } from '../reducers/savelist'; 8 | 9 | export const setCurrentPage = (page: number = 1) => ({ 10 | type: SET_CURRENT_PAGE, 11 | payload: page, 12 | }); 13 | 14 | export const updateSavelist = (codes: Array) => ({ 15 | type: SET_SAVELIST, 16 | payload: codes, 17 | }); 18 | 19 | export const setPageMetaData = ( 20 | count: number, 21 | next: number | null, 22 | previous: number | null, 23 | total_pages: number, 24 | ) => ({ 25 | type: SET_PAGE_METADATA, 26 | payload: { 27 | count, 28 | next, 29 | previous, 30 | total_pages, 31 | }, 32 | }); 33 | 34 | export const clearSavelist = () => ({ 35 | type: CLEAR_SAVELIST, 36 | }); 37 | -------------------------------------------------------------------------------- /src/initializers/code.ts: -------------------------------------------------------------------------------- 1 | import { batch } from 'react-redux'; 2 | import { Dispatch } from 'redux'; 3 | import { getCodeById } from '~/services/ide'; 4 | import { setSource, setStdin, setLanguageById, setFilename } from '~/store/action/editor'; 5 | 6 | export async function fetchCodeFromIdParam(dispatch: Dispatch) { 7 | const queryParams = new URLSearchParams(location.search); 8 | const id = queryParams.get('id'); 9 | 10 | if (!id) { 11 | return; 12 | } 13 | 14 | try { 15 | const response = await getCodeById(id); 16 | const { data } = response; 17 | const source = atob(data.source); 18 | batch(() => { 19 | dispatch(setLanguageById(data.lang)); 20 | dispatch(setSource(source)); 21 | dispatch(setStdin(data.input)); 22 | dispatch(setFilename(data.title || 'Untitled')); 23 | }); 24 | } catch (error) { 25 | console.error('Fetch code error = ', error); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/store/reducers/auth.ts: -------------------------------------------------------------------------------- 1 | import { LOGIN, LOGOUT } from '../action-types/auth'; 2 | 3 | interface UserType { 4 | email: string; 5 | first_name: string; 6 | last_name: string; 7 | } 8 | 9 | export interface AuthState { 10 | user: UserType | null; 11 | jwt: string; 12 | isLoggedIn: boolean; 13 | } 14 | 15 | const initialState = { 16 | user: null, 17 | jwt: '', 18 | isLoggedIn: false, 19 | }; 20 | 21 | export const authReducer = (state: AuthState = initialState, action): AuthState => { 22 | switch (action.type) { 23 | case LOGIN: 24 | return { 25 | ...state, 26 | user: action.payload.user, 27 | jwt: action.payload.jwt, 28 | isLoggedIn: true, 29 | }; 30 | case LOGOUT: 31 | return { 32 | ...state, 33 | user: null, 34 | isLoggedIn: false, 35 | jwt: '', 36 | }; 37 | default: 38 | return state; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/editor/fontsize-picker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FONTSIZE_MAP } from '~/constants/fontsizes'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { getFontSize } from '~/store/getters/editor'; 5 | import { setFontSize } from '~/store/action/editor'; 6 | 7 | interface Props {} 8 | 9 | export const FontsizePicker: React.FC = () => { 10 | const dispatch = useDispatch(); 11 | const fontSize = useSelector(getFontSize()); 12 | 13 | function onChange(e) { 14 | const value = e.target.value; 15 | dispatch(setFontSize(value)); 16 | } 17 | 18 | return ( 19 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/tasks/save-update-code.ts: -------------------------------------------------------------------------------- 1 | import { saveCode, updateCode } from '~/services/ide'; 2 | 3 | interface codeData { 4 | lang: string; 5 | source: string; 6 | input: string; 7 | } 8 | 9 | export async function saveUpdateCode(data: codeData, id?: string) { 10 | data.source = btoa(data.source); 11 | if (!id) { 12 | // This is a fresh code. Save it as new. 13 | try { 14 | const response = await saveCode(data); 15 | const { id } = response.data; 16 | return { 17 | id, 18 | }; 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | } else { 23 | // This is a previously saved code as the ID exists. The user wants to update this. 24 | try { 25 | const response = await updateCode(id, data); 26 | const newId = response.data.id; 27 | 28 | return { 29 | id: newId, 30 | }; 31 | } catch (error) { 32 | console.error(error); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/editor/code-editor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Editor from '@monaco-editor/react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | 5 | import { getCurrentSource, getSelectedLanguage, getFontSize } from '~/store/getters/editor'; 6 | import { setSource } from '~/store/action/editor'; 7 | 8 | export const CodeEditor = () => { 9 | const dispatch = useDispatch(); 10 | 11 | const selectedLanguage = useSelector(getSelectedLanguage()); 12 | const source = useSelector(getCurrentSource()); 13 | const fontSize = useSelector(getFontSize()); 14 | const editor_code = selectedLanguage?.editor_code || ''; 15 | 16 | const setEditorSource = (newSource) => dispatch(setSource(newSource)); 17 | 18 | return ( 19 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/editor/language-picker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { getSelectedLanguage, getLanguages } from '~/store/getters/editor'; 4 | import { setLanguageById } from '~/store/action/editor'; 5 | import { Language } from '~/store/reducers/editor'; 6 | 7 | export const LanguagePicker: React.FC = () => { 8 | const dispatch = useDispatch(); 9 | const languages = useSelector(getLanguages()); 10 | const selectedLanguage = useSelector(getSelectedLanguage()); 11 | const selectLanguageById = (id) => dispatch(setLanguageById(id)); 12 | 13 | return ( 14 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/base/modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { setActiveModal } from '~/store/action/ui'; 4 | import { getActiveModalName } from '~/store/getters/ui'; 5 | 6 | export interface BaseModalProps { 7 | name: string; 8 | style: string; 9 | children: ReactElement[]; 10 | } 11 | 12 | export const BaseModal: React.FC = (props) => { 13 | const dispatch = useDispatch(); 14 | const activeModalName = useSelector(getActiveModalName()); 15 | 16 | const { style } = props; 17 | 18 | const toggleOverlay = (e) => { 19 | if (e.target === e.currentTarget) { 20 | dispatch(setActiveModal(null)); 21 | } 22 | }; 23 | const overlayClassName = activeModalName === props.name ? '' : 'overlay--hidden'; 24 | 25 | return ( 26 |
27 |
28 |
{props.children}
29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/store/action/ui.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TOGGLE_IO_PANE, 3 | SET_ACTIVE_MODAL, 4 | TOGGLE_BANNER, 5 | TOGGLE_OPTIONS_MENU, 6 | SET_ACTIVE_PANEL, 7 | SET_TOAST, 8 | } from '~/store/action-types/ui'; 9 | import { Toast, ToastType } from '~/store/reducers/ui'; 10 | 11 | export const toggleIOPane = () => ({ 12 | type: TOGGLE_IO_PANE, 13 | }); 14 | 15 | export const setActiveModal = (modalName?: string) => ({ 16 | type: SET_ACTIVE_MODAL, 17 | payload: modalName || null, 18 | }); 19 | 20 | export const toggleBanner = () => ({ 21 | type: TOGGLE_BANNER, 22 | }); 23 | 24 | export const toggleOptionsMenu = () => ({ 25 | type: TOGGLE_OPTIONS_MENU, 26 | }); 27 | 28 | export const setActivePanel = (panel?: string) => ({ 29 | type: SET_ACTIVE_PANEL, 30 | payload: panel || null, 31 | }); 32 | 33 | export const setToast = (toast: Partial) => { 34 | const payload = toast 35 | ? { 36 | message: '', 37 | timeout: 3000, 38 | type: ToastType.SUCCESS, 39 | ...toast, 40 | } 41 | : null; 42 | return { 43 | type: SET_TOAST, 44 | payload, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Coding Minutes | IDE 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /src/store/reducers/savelist.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_SAVELIST, 3 | SET_CURRENT_PAGE, 4 | CLEAR_SAVELIST, 5 | SET_PAGE_METADATA, 6 | } from '../action-types/savelist'; 7 | 8 | export interface Codefile { 9 | source: string; 10 | lang: number; 11 | user_email: string; 12 | id: string; 13 | input: string; 14 | title: string; 15 | } 16 | 17 | export interface SavelistState { 18 | page: number; 19 | codes: Codefile[]; 20 | count: number; 21 | next: number | null; 22 | previous: number | null; 23 | total_pages: number; 24 | } 25 | 26 | const initialState: SavelistState = { 27 | page: 1, 28 | codes: [], 29 | count: 0, 30 | next: null, 31 | previous: null, 32 | total_pages: 0, 33 | }; 34 | 35 | export const savelistReducer = (state: SavelistState = initialState, action): SavelistState => { 36 | switch (action.type) { 37 | case SET_CURRENT_PAGE: 38 | return { 39 | ...state, 40 | page: action.payload, 41 | }; 42 | case SET_SAVELIST: 43 | return { 44 | ...state, 45 | codes: action.payload, 46 | }; 47 | case SET_PAGE_METADATA: 48 | return { 49 | ...state, 50 | ...action.payload, 51 | }; 52 | case CLEAR_SAVELIST: 53 | return initialState; 54 | 55 | default: 56 | return state; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/layout/screen-size-modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { BaseModal } from '~/components/base/modal'; 4 | import { SCREEN_SIZE_MODAL } from '~/constants/modal'; 5 | 6 | export const ScreenSizeModal: React.FC = () => { 7 | return ( 8 | 9 |
SORRY :(
10 | Coding Minutes Logo 16 |
17 | IDE doesn't support screen widths smaller than 768px at the moment! To give 18 | you an idea why, we've kept every non-responsive piece transparent in the background! 19 |
20 | YUP! Pretty broken. :) 21 |
22 |
23 |
24 | Move to a bigger screen size, fix your aspect ratio or just zoom out to sort this issue. 25 | IDE's shouldn't have mobile views in the first place but we're working on it for you. Hope 26 | you had fun interacting with a fellow dev who wanted to vent. 27 |
28 | Thanks for hearing me out! HAPPY CODING. :) 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/editor/io-nav/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { getReturnCode, getStdout } from '~/store/getters/editor'; 4 | import { getActivePanel } from '~/store/getters/ui'; 5 | import { IO_PANEL, SAVELIST_PANEL } from '~/constants/panel'; 6 | 7 | import { InputFragment } from './input-fragment'; 8 | import { OutputFragment } from './output-fragment'; 9 | import Savelist from './savelist'; 10 | 11 | enum IOTabs { 12 | INPUT, 13 | CONSOLE, 14 | } 15 | 16 | export interface IONavProps { 17 | className: string; 18 | } 19 | 20 | export const IONav: React.FC = (props) => { 21 | const { className } = props; 22 | 23 | const stdout = useSelector(getStdout()); 24 | const returnCode = useSelector(getReturnCode()); 25 | const [selectedTab, setSelectedTab] = React.useState(stdout ? IOTabs.CONSOLE : IOTabs.INPUT); 26 | const activePanel = useSelector(getActivePanel()); 27 | 28 | React.useEffect(() => { 29 | if (returnCode !== null) { 30 | setSelectedTab(IOTabs.CONSOLE); 31 | } 32 | }, [returnCode]); 33 | 34 | return ( 35 |
36 | {activePanel == SAVELIST_PANEL && } 37 | {activePanel == IO_PANEL && ( 38 |
39 | 40 | 41 |
42 | )} 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [12.x] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Install Packages 19 | run: npm install 20 | - name: Build page 21 | run: npm run build 22 | env: 23 | API_HOST: 'https://ide-backend.codingminutes.com' 24 | NODE_ENV: 'production' 25 | REACT_APP_GOOGLE_CLIENT_ID: ${{ secrets.REACT_APP_GOOGLE_CLIENT_ID }} 26 | REACT_APP_GITHUB_CLIENT_ID: ${{ secrets.REACT_APP_GITHUB_CLIENT_ID }} 27 | - name: Upload Build 28 | uses: appleboy/scp-action@master 29 | with: 30 | host: ${{ secrets.HOST }} 31 | username: ${{ secrets.USERNAME }} 32 | key: ${{ secrets.KEY }} 33 | source: 'dist' 34 | target: 'temp' 35 | - name: Replace New Build 36 | uses: appleboy/ssh-action@master 37 | with: 38 | host: ${{ secrets.HOST }} 39 | username: ${{ secrets.USERNAME }} 40 | key: ${{ secrets.KEY }} 41 | script: rm -rf ~/frontends/ide/* && cp -rf ~/temp/dist/* ~/frontends/ide && rm -rf ~/temp 42 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const mode = process.env.NODE_ENV || 'development'; 5 | 6 | require('dotenv').config(); 7 | 8 | module.exports = { 9 | entry: './src/index.tsx', 10 | mode, 11 | output: { 12 | path: path.resolve(__dirname, 'dist'), 13 | filename: mode === 'production' ? 'bundle-[contenthash].js' : 'bundle.js', 14 | chunkFilename: mode === 'production' ? '[id]-[chunkhash].js' : '[id].js', 15 | publicPath: '/', 16 | }, 17 | resolve: { 18 | alias: { 19 | '~': __dirname + '/src', 20 | }, 21 | extensions: ['.tsx', '.ts', '.js'], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.[tj]sx?$/, 27 | use: [ 28 | { 29 | loader: 'ts-loader', 30 | options: { 31 | configFile: 'tsconfig.json', 32 | }, 33 | }, 34 | ], 35 | exclude: /node_modules/, 36 | }, 37 | { 38 | test: /\.s[ac]ss$/, 39 | use: ['style-loader', 'css-loader', 'sass-loader'], 40 | exclude: /node_modules/, 41 | }, 42 | ], 43 | }, 44 | plugins: [ 45 | new webpack.DefinePlugin({ 46 | 'process.env': Object.keys(process.env).reduce((acc, curr) => { 47 | acc[curr] = JSON.stringify(process.env[curr]); 48 | return acc; 49 | }, {}), 50 | }), 51 | new HtmlWebpackPlugin({ 52 | template: __dirname + '/src/index.html', 53 | filename: 'index.html', 54 | inject: 'body', 55 | }), 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /src/store/action/editor.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '~/store/reducers/editor'; 2 | import { 3 | SET_RETURN_CODE, 4 | SET_SELECTED_LANGUAGE_BY_ID, 5 | SET_SOURCE, 6 | SET_STDIN, 7 | SET_STDOUT, 8 | SET_STUBS, 9 | SET_LANGUAGES, 10 | SET_FONT_SIZE, 11 | SET_FILENAME, 12 | } from '~/store/action-types/editor'; 13 | import { listToMap } from '~/utils/store'; 14 | import { setSettings } from '~/services/settings'; 15 | 16 | interface CodeStub { 17 | language_id: number; 18 | stub: string; 19 | } 20 | 21 | export const setSource = (source: string) => ({ 22 | type: SET_SOURCE, 23 | payload: source, 24 | }); 25 | 26 | export const setStdout = (stdout: string) => ({ 27 | type: SET_STDOUT, 28 | payload: stdout, 29 | }); 30 | 31 | export const setStdin = (stdin: string) => ({ 32 | type: SET_STDIN, 33 | payload: stdin, 34 | }); 35 | 36 | export const setReturnCode = (returnCode: number) => ({ 37 | type: SET_RETURN_CODE, 38 | payload: returnCode, 39 | }); 40 | 41 | export const setCodeStubs = (codeStubs: Array) => ({ 42 | type: SET_STUBS, 43 | payload: listToMap(codeStubs, 'language_id', (obj) => obj.stub), 44 | }); 45 | 46 | export const setLanguages = (languages: Array) => ({ 47 | type: SET_LANGUAGES, 48 | payload: languages, 49 | }); 50 | 51 | export const setLanguageById = (id: string | number) => { 52 | setSettings({ lang_id: id }); 53 | return { 54 | type: SET_SELECTED_LANGUAGE_BY_ID, 55 | payload: id, 56 | }; 57 | }; 58 | 59 | export const setFontSize = (size: string) => { 60 | setSettings({ fontSize: size }); 61 | return { 62 | type: SET_FONT_SIZE, 63 | payload: size, 64 | }; 65 | }; 66 | 67 | export const setFilename = (filename: string) => ({ 68 | type: SET_FILENAME, 69 | payload: filename, 70 | }); 71 | -------------------------------------------------------------------------------- /src/store/reducers/ui.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TOGGLE_IO_PANE, 3 | SET_ACTIVE_MODAL, 4 | TOGGLE_BANNER, 5 | TOGGLE_OPTIONS_MENU, 6 | SET_ACTIVE_PANEL, 7 | SET_TOAST, 8 | } from '~/store/action-types/ui'; 9 | 10 | export enum ToastType { 11 | DANGER, 12 | SUCCESS, 13 | WARNING, 14 | INFO, 15 | } 16 | 17 | export interface Toast { 18 | message: string; 19 | type: ToastType; 20 | timeout: number; 21 | } 22 | 23 | export interface UIState { 24 | io_pane_open: boolean; 25 | active_modal: string | null; 26 | isBannerVisible: boolean; 27 | isOptionsMenuOpen: boolean; 28 | active_panel: string | null; 29 | toast: Toast | null; 30 | } 31 | 32 | const initialState: UIState = { 33 | io_pane_open: true, 34 | active_modal: null, 35 | isBannerVisible: false, 36 | isOptionsMenuOpen: false, 37 | active_panel: '', 38 | toast: null, 39 | }; 40 | 41 | export const uiReducer = (state: UIState = initialState, action): UIState => { 42 | switch (action.type) { 43 | case TOGGLE_IO_PANE: 44 | return { 45 | ...state, 46 | io_pane_open: !state.io_pane_open, 47 | }; 48 | case SET_ACTIVE_MODAL: 49 | return { 50 | ...state, 51 | active_modal: action.payload, 52 | }; 53 | 54 | case TOGGLE_BANNER: 55 | return { 56 | ...state, 57 | isBannerVisible: !state.isBannerVisible, 58 | }; 59 | case TOGGLE_OPTIONS_MENU: 60 | return { 61 | ...state, 62 | isOptionsMenuOpen: !state.isOptionsMenuOpen, 63 | }; 64 | case SET_ACTIVE_PANEL: 65 | return { 66 | ...state, 67 | active_panel: action.payload, 68 | }; 69 | 70 | case SET_TOAST: 71 | return { 72 | ...state, 73 | toast: action.payload, 74 | }; 75 | default: 76 | return state; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ide", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Jatin Katyal ", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "webpack", 9 | "dev": "webpack serve", 10 | "lint": "eslint --fix './src/**/*.{ts,tsx,js}'", 11 | "prettier": "prettier --write --loglevel silent ./src" 12 | }, 13 | "dependencies": { 14 | "@monaco-editor/react": "^4.1.3", 15 | "axios": "^0.21.1", 16 | "css-loader": "^5.2.4", 17 | "file-loader": "^6.2.0", 18 | "jwt-decode": "^3.1.2", 19 | "node-sass": "^6.0.0", 20 | "react": "^17.0.2", 21 | "react-dom": "^17.0.2", 22 | "react-google-login": "^5.2.2", 23 | "react-oauth-popup": "^1.0.5", 24 | "react-redux": "^7.2.4", 25 | "react-router-dom": "^5.2.0", 26 | "react-use-task": "^0.0.3", 27 | "redux": "^4.1.0", 28 | "redux-devtools-extension": "^2.13.9", 29 | "sass-loader": "^11.1.1", 30 | "style-loader": "^2.0.0", 31 | "swr": "^1.0.0" 32 | }, 33 | "devDependencies": { 34 | "@types/axios": "^0.14.0", 35 | "@typescript-eslint/eslint-plugin": "^4.23.0", 36 | "@typescript-eslint/parser": "^4.23.0", 37 | "dotenv": "^10.0.0", 38 | "eslint": "^7.26.0", 39 | "eslint-config-airbnb": "^18.2.1", 40 | "eslint-config-prettier": "^8.3.0", 41 | "eslint-plugin-import": "^2.23.2", 42 | "eslint-plugin-jsx-a11y": "^6.4.1", 43 | "eslint-plugin-react": "^7.23.2", 44 | "eslint-plugin-react-hooks": "^4.2.0", 45 | "html-webpack-plugin": "^5.3.1", 46 | "husky": "^6.0.0", 47 | "lint-staged": "^11.0.0", 48 | "prettier": "^2.3.0", 49 | "ts-loader": "^9.1.2", 50 | "typescript": "^4.2.4", 51 | "webpack": "^5.37.0", 52 | "webpack-cli": "^4.7.0", 53 | "webpack-dev-server": "^3.11.2" 54 | }, 55 | "husky": { 56 | "hooks": { 57 | "pre-commit": "lint-staged" 58 | } 59 | }, 60 | "lint-staged": { 61 | "src/**/*.{js,ts,tsx}": [ 62 | "eslint --fix", 63 | "prettier --write --loglevel silent" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /src/store/reducers/editor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_RETURN_CODE, 3 | SET_SOURCE, 4 | SET_STDIN, 5 | SET_STDOUT, 6 | SET_STUBS, 7 | SET_LANGUAGES, 8 | SET_SELECTED_LANGUAGE_BY_ID, 9 | SET_FONT_SIZE, 10 | SET_FILENAME, 11 | } from '~/store/action-types/editor'; 12 | import { listToMap } from '~/utils/store'; 13 | import { FONTSIZE_MAP } from '~/constants/fontsizes'; 14 | 15 | export interface Language { 16 | id: number; 17 | name: number; 18 | } 19 | 20 | export interface EditorState { 21 | selectedLanguage: Language | null; 22 | sourceLanguageMap: { [key: number]: string }; 23 | stdin: string; 24 | stdout: string; 25 | returnCode: number; 26 | languages: { [id: number]: Language }; 27 | fontSize: string; 28 | filename: string; 29 | } 30 | 31 | const initialState: EditorState = { 32 | selectedLanguage: null, 33 | sourceLanguageMap: {}, 34 | stdin: '', 35 | stdout: '', 36 | returnCode: null, 37 | languages: {}, 38 | fontSize: FONTSIZE_MAP.SMALL, 39 | filename: 'Untitled', 40 | }; 41 | 42 | export const editorReducer = (state: EditorState = initialState, action): EditorState => { 43 | switch (action.type) { 44 | case SET_STUBS: 45 | return { 46 | ...state, 47 | sourceLanguageMap: action.payload, 48 | }; 49 | case SET_SOURCE: 50 | return { 51 | ...state, 52 | sourceLanguageMap: { 53 | ...state.sourceLanguageMap, 54 | [state.selectedLanguage.id]: action.payload, 55 | }, 56 | }; 57 | case SET_STDOUT: 58 | return { 59 | ...state, 60 | stdout: action.payload, 61 | }; 62 | case SET_RETURN_CODE: 63 | return { 64 | ...state, 65 | returnCode: action.payload, 66 | }; 67 | case SET_STDIN: 68 | return { 69 | ...state, 70 | stdin: action.payload, 71 | }; 72 | case SET_LANGUAGES: 73 | return { 74 | ...state, 75 | languages: listToMap(action.payload), 76 | // Set default language when no language is selected. 77 | ...(!state.selectedLanguage 78 | ? { 79 | selectedLanguage: action.payload[0], 80 | } 81 | : {}), 82 | }; 83 | case SET_SELECTED_LANGUAGE_BY_ID: 84 | return { 85 | ...state, 86 | selectedLanguage: state.languages[action.payload], 87 | }; 88 | case SET_FONT_SIZE: 89 | return { 90 | ...state, 91 | fontSize: action.payload, 92 | }; 93 | 94 | case SET_FILENAME: 95 | return { 96 | ...state, 97 | filename: action.payload, 98 | }; 99 | 100 | default: 101 | return state; 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/views/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTask } from 'react-use-task'; 3 | import { Navbar } from '~/components/layout/navbar'; 4 | import { CodeEditor } from '~/components/editor/code-editor'; 5 | import { IONav } from '~/components/editor/io-nav'; 6 | import { FabState, RunFAB } from '~/components/editor/run-fab'; 7 | import { useDispatch, useSelector } from 'react-redux'; 8 | import { getReturnCode } from '~/store/getters/editor'; 9 | import { setReturnCode } from '~/store/action/editor'; 10 | import { isIOPaneOpen, isBannerVisible } from '~/store/getters/ui'; 11 | import { initialize } from '~/initializers'; 12 | import { toggleIOPane } from '~/store/action/ui'; 13 | import { LoginModal } from '~/components/auth/login-modal'; 14 | import { ScreenSizeModal } from '~/components/layout/screen-size-modal'; 15 | import { Banner } from '~/components/layout/banner'; 16 | import { Toast } from '~/components/base/toast'; 17 | import banner_data from '~/data/banner.json'; 18 | 19 | const getFabStateForReturnCode = (returnCode: number | null): FabState => { 20 | if (returnCode === 0) return FabState.correct; 21 | 22 | if (Number.isInteger(returnCode) && returnCode !== 0) return FabState.error; 23 | 24 | return FabState.idle; 25 | }; 26 | 27 | function pickRandomBannerData(data) { 28 | const keys = Object.keys(data); 29 | return data[keys[(keys.length * Math.random()) << 0]]; 30 | } 31 | 32 | export const MainView: React.FC = () => { 33 | const dispatch = useDispatch(); 34 | const returnCode = useSelector(getReturnCode()); 35 | const isIOOpen = useSelector(isIOPaneOpen()); 36 | const showBanner = useSelector(isBannerVisible()); 37 | const bannerContent = pickRandomBannerData(banner_data); 38 | 39 | const state = getFabStateForReturnCode(returnCode); 40 | 41 | const toggleIO = () => dispatch(toggleIOPane()); 42 | 43 | const [{}, perform] = useTask(initialize); 44 | 45 | React.useEffect(() => { 46 | perform(dispatch); 47 | }, []); 48 | 49 | React.useEffect(() => { 50 | if (state != FabState.idle) { 51 | setTimeout(() => { 52 | dispatch(setReturnCode(null)); 53 | }, 2000); 54 | } 55 | }, [state]); 56 | 57 | return ( 58 | <> 59 |
60 | {showBanner && } 61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 | 69 |
70 |
71 | 72 | 73 |
74 |
75 | 76 |
77 | 78 | {/*
Toggle I/O pane
*/} 79 |
80 |
81 |
82 | 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /src/components/editor/run-fab/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { batch, useDispatch, useSelector } from 'react-redux'; 3 | import { useTask } from 'react-use-task'; 4 | 5 | import { pollSubmission, Submission } from '~/components/editor/run-fab/actions'; 6 | import { sleep } from '~/components/editor/run-fab/utils'; 7 | import { runCode } from '~/services/judge'; 8 | import { getCurrentSource, getSelectedLanguage, getStdin } from '~/store/getters/editor'; 9 | import { setReturnCode, setStdout } from '~/store/action/editor'; 10 | 11 | const MAX_RETRIES = 20; 12 | 13 | export enum FabState { 14 | idle = 'idle', 15 | correct = 'correct', 16 | error = 'error', 17 | } 18 | export interface RunFabProps { 19 | state: FabState; 20 | } 21 | 22 | export const RunFAB = (props: RunFabProps) => { 23 | const dispatch = useDispatch(); 24 | 25 | const currentSource = useSelector(getCurrentSource()); 26 | const selectedLanguage = useSelector(getSelectedLanguage()); 27 | const currentInput = useSelector(getStdin()); 28 | 29 | const setOutput = (stdout) => dispatch(setStdout(stdout)); 30 | const setCode = (returnCode) => dispatch(setReturnCode(returnCode)); 31 | 32 | const [{ isRunning }, perform] = useTask(function* (source, language, input) { 33 | const response: any = yield runCode({ 34 | source_code: btoa(source), 35 | language_id: language, 36 | stdin: btoa(input), 37 | }); 38 | 39 | const submissionId = response.data.submission_id; 40 | 41 | let retry = MAX_RETRIES; 42 | while (retry--) { 43 | yield sleep(1000); 44 | const submission: Submission = yield pollSubmission(submissionId); 45 | 46 | if (submission.time || submission.compile_output || submission.stderr || submission.stdout) { 47 | batch(() => { 48 | setOutput(submission.compile_output || submission.stderr || submission.stdout); 49 | setCode(submission.stderr || submission.compile_output ? 1 : 0); 50 | }); 51 | return; 52 | } 53 | } 54 | }); 55 | 56 | const execute = () => perform(currentSource, selectedLanguage.id, currentInput); 57 | 58 | const ButtonClassMap = { 59 | idle: 'run-button--run-code', 60 | correct: 'run-button--success', 61 | error: 'run-button--error', 62 | }; 63 | 64 | const IconMap = { 65 | idle: 'https://minio.codingminutes.com/assets/play-white.svg', 66 | correct: 'https://minio.codingminutes.com/assets/play-white.svg', 67 | error: 'https://minio.codingminutes.com/assets/play-white.svg', 68 | }; 69 | 70 | const buttonClass = ButtonClassMap[props.state]; 71 | const icon = IconMap[props.state]; 72 | 73 | return ( 74 | 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /src/components/editor/io-nav/savelist.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { 4 | getCurrentPageNumber, 5 | getCurrentPageList, 6 | getNextPageNumber, 7 | getPreviousPageNumber, 8 | getTotalPages, 9 | } from '~/store/getters/savelist'; 10 | import { fetchSavedCodes } from '~/tasks/savecodelist'; 11 | import { setCurrentPage } from '~/store/action/savelist'; 12 | import { getLanguageMap } from '~/store/getters/editor'; 13 | 14 | const Savelist = (props) => { 15 | const [query, setQuery] = React.useState(''); 16 | const dispatch = useDispatch(); 17 | const savelist = useSelector(getCurrentPageList()); 18 | const currentPageNumber = useSelector(getCurrentPageNumber()); 19 | const totalPages = useSelector(getTotalPages()); 20 | const previousPage = useSelector(getPreviousPageNumber()); 21 | const nextPage = useSelector(getNextPageNumber()); 22 | const languageMap = useSelector(getLanguageMap()); 23 | 24 | fetchSavedCodes(dispatch, currentPageNumber, query); 25 | 26 | function changePageNumber(page) { 27 | dispatch(setCurrentPage(page)); 28 | } 29 | 30 | return ( 31 | <> 32 |
33 |
34 |
35 |
Saved Codes
36 |
37 |
38 | setQuery(e.target.value)} 43 | className="transparent-input w-100" 44 | /> 45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {savelist && 69 | savelist.map((code) => ( 70 | 71 | 72 | 73 | 74 | 80 | 81 | ))} 82 | 83 |
NameLanguageLast Updated
{code.title}{languageMap[code.lang]?.name}{new Date(code.updated_at).toLocaleString()}
84 |
85 |
86 | 93 |
94 | Showing {currentPageNumber} of {totalPages} 95 |
96 | 103 |
104 |
105 | 106 | ); 107 | }; 108 | 109 | export default Savelist; 110 | -------------------------------------------------------------------------------- /src/components/auth/login-modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, batch } from 'react-redux'; 3 | import { GoogleLogin } from 'react-google-login'; 4 | import OauthPopup from 'react-oauth-popup'; 5 | import jwt_decode from "jwt-decode" 6 | import { LOGIN_MODAL } from '~/constants/modal'; 7 | import { loginUser } from '~/store/action/auth'; 8 | import { BaseModal } from '~/components/base/modal'; 9 | import Config from '~/config'; 10 | import { loginWithGoogle, loginWithGithub } from '~/services/auth'; 11 | import { setActiveModal } from '~/store/action/ui'; 12 | import { setJwt } from '~/utils/jwt'; 13 | 14 | export const LoginModal: React.FC = () => { 15 | const dispatch = useDispatch(); 16 | const [loading, setLoading] = React.useState(false); 17 | 18 | const loginGoogleSuccess = async (response) => { 19 | try { 20 | setLoading(true); 21 | const token = response.getAuthResponse().id_token; 22 | const res = await loginWithGoogle(token); 23 | const { jwt } = res; 24 | 25 | const basicProfile = response.getBasicProfile(); 26 | const user = { 27 | email: basicProfile.getEmail(), 28 | first_name: basicProfile.getGivenName(), 29 | last_name: basicProfile.getFamilyName(), 30 | }; 31 | 32 | batch(() => { 33 | dispatch( 34 | loginUser({ 35 | user, 36 | jwt, 37 | isLoggedIn: true, 38 | }), 39 | ); 40 | dispatch(setActiveModal(null)); 41 | }); 42 | setJwt(jwt); 43 | } catch (error) { 44 | console.error(error); 45 | } finally { 46 | setLoading(false); 47 | } 48 | }; 49 | 50 | const loginGithubSuccess = async (code, params) => { 51 | try { 52 | setLoading(true); 53 | const res = await loginWithGithub(code); 54 | const { jwt } = res; 55 | const decoded :any = jwt_decode(jwt); 56 | 57 | const user = { 58 | email: decoded.email, 59 | first_name: decoded.first_name, 60 | last_name: decoded.last_name, 61 | }; 62 | 63 | batch(() => { 64 | dispatch( 65 | loginUser({ 66 | user, 67 | jwt, 68 | isLoggedIn: true, 69 | }), 70 | ); 71 | dispatch(setActiveModal(null)); 72 | }); 73 | setJwt(jwt); 74 | } catch (error) { 75 | console.error(error); 76 | } finally { 77 | setLoading(false); 78 | } 79 | } 80 | 81 | const loginFailure = (e) => { 82 | e?.preventDefault?.(); 83 | console.error('Failure = ', e); 84 | }; 85 | 86 | function getGithubOAuthURL() { 87 | const GITHUB_CLIENT_ID = Config.GITHUB_CLIENT_ID; 88 | const scopes = "read:user+user:email"; 89 | const redirect_uri = window.location.href; 90 | const url = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&scope=${scopes}&redirect_uri=${redirect_uri}`; 91 | return url; 92 | } 93 | 94 | return ( 95 | 96 |
Welcome to
97 | Coding Minutes Logo 102 |
103 |
Login to your account
104 | {loading &&

Loading...

} 105 | {!loading && ( 106 | <> 107 |
108 | ; }) => ( 113 |
114 | Google 115 |
Continue with Google
116 |
117 | )} 118 | /> 119 |
120 | {}} 124 | title="Github OAuth" 125 | width={800} 126 | height={700} 127 | > 128 |
129 |
130 | Github 131 |
Continue with Github
132 |
133 |
134 |
135 | 136 | )} 137 |
138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /src/components/layout/navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { useDispatch, useSelector, batch } from 'react-redux'; 4 | import { LanguagePicker } from '~/components/editor/language-picker'; 5 | import { getIsLoggedIn, getUser } from '~/store/getters/auth'; 6 | import { logoutUser } from '~/store/action/auth'; 7 | import { setActiveModal, setActivePanel, setToast, toggleOptionsMenu } from '~/store/action/ui'; 8 | import { LOGIN_MODAL } from '~/constants/modal'; 9 | import { 10 | getCurrentSource, 11 | getStdin, 12 | getSelectedLanguage, 13 | getFilename, 14 | } from '~/store/getters/editor'; 15 | import { clearJwt } from '~/utils/jwt'; 16 | import { saveUpdateCode } from '~/tasks/save-update-code'; 17 | import { setFilename } from '~/store/action/editor'; 18 | import { getActivePanel, isOptionsMenuOpen } from '~/store/getters/ui'; 19 | import { IO_PANEL, SAVELIST_PANEL } from '~/constants/panel'; 20 | import { FontsizePicker } from '~/components/editor/fontsize-picker'; 21 | 22 | export const Navbar: React.FC = () => { 23 | const [loading, setLoading] = React.useState(false); 24 | const dispatch = useDispatch(); 25 | const isLoggedIn = useSelector(getIsLoggedIn()); 26 | const user = useSelector(getUser()); 27 | const currentLanguage = useSelector(getSelectedLanguage()); 28 | const filename = useSelector(getFilename()); 29 | const data = { 30 | lang: currentLanguage?.id || -1, 31 | source: useSelector(getCurrentSource()), 32 | input: useSelector(getStdin()), 33 | title: filename, 34 | }; 35 | const isMenuOpen = useSelector(isOptionsMenuOpen()); 36 | const activePanel = useSelector(getActivePanel()); 37 | 38 | const history = useHistory(); 39 | 40 | 41 | function toggleMenu() { 42 | dispatch(toggleOptionsMenu()); 43 | } 44 | 45 | const toggleOverlay = () => { 46 | batch(() => { 47 | toggleMenu(); 48 | dispatch(setActiveModal(LOGIN_MODAL)); 49 | }); 50 | } 51 | 52 | function changePanel(panel) { 53 | batch(() => { 54 | dispatch(setActivePanel(panel)); 55 | toggleMenu(); 56 | }); 57 | } 58 | 59 | const logout = () => { 60 | dispatch(logoutUser()); 61 | clearJwt(); 62 | }; 63 | 64 | async function saveCode() { 65 | if (!isLoggedIn) { 66 | return toggleOverlay(); 67 | } 68 | setLoading(true); 69 | const queryParams = new URLSearchParams(window.location.search); 70 | const id = queryParams.get('id'); 71 | try { 72 | const response = await saveUpdateCode(data, id); 73 | if (id != response?.id) { 74 | return history.push(`/?id=${response.id}`); 75 | } 76 | } catch (error) { 77 | console.error(error); 78 | } finally { 79 | setLoading(false); 80 | dispatch( 81 | setToast({ 82 | message: 'Code saved successfully', 83 | }), 84 | ); 85 | } 86 | } 87 | 88 | function copyCode() { 89 | const sourceCode = data.source; 90 | navigator.clipboard.writeText(sourceCode); 91 | } 92 | 93 | function changeFilename(event) { 94 | const value = event?.target?.value; 95 | dispatch(setFilename(value)); 96 | } 97 | 98 | return ( 99 |
100 |
101 |
102 |
103 | 104 | Coding Minutes Logo 109 | 110 | 111 |
112 | 113 | 128 |
129 | 130 | 136 | 140 | New 141 | 142 | 143 |
144 | 148 | Copy Code 149 |
150 | {/*
Share
*/} 151 |
152 |
153 | 154 | 155 | {isLoggedIn && ( 156 |
157 |
161 |
162 | Hi, {user.first_name} {user.last_name} 163 |
164 |
165 | {isMenuOpen && ( 166 |
167 | {activePanel != SAVELIST_PANEL && ( 168 | changePanel(SAVELIST_PANEL)} 171 | > 172 | 176 |
Saved Codes
177 |
178 | )} 179 | {activePanel != IO_PANEL && ( 180 | changePanel(IO_PANEL)} 183 | > 184 |
I/O Pane
185 |
186 | )} 187 | 194 |
195 | )} 196 |
197 | )} 198 | {!isLoggedIn && ( 199 | 202 | )} 203 |
204 |
205 |
206 |
207 | ); 208 | }; 209 | -------------------------------------------------------------------------------- /src/style/app.scss: -------------------------------------------------------------------------------- 1 | // Base Classes 2 | %button-generic { 3 | font-weight: 700; 4 | padding: 0.5rem 1.75rem; 5 | border-radius: 5px; 6 | 7 | &:focus { 8 | border: unset; 9 | box-shadow: unset; 10 | outline: unset; 11 | } 12 | } 13 | 14 | // Variables 15 | $accent-color-green: #7ac25b; 16 | $accent-color-purple: #5848ea; 17 | $accent-color-pink: #9549eb; 18 | 19 | $secondary-color-green: #6cc644; 20 | 21 | $accent-gradient-purple: linear-gradient(to right, $accent-color-purple, $accent-color-pink); 22 | $button-gradient: linear-gradient( 23 | to right, 24 | $accent-color-purple, 25 | $accent-color-pink, 26 | $accent-color-purple 27 | ); 28 | 29 | $primary-dark: #1e1e1e; 30 | $overlay-dark: #25282e; 31 | $secondary-dark: #404040; 32 | $tertiary-dark: #5c5c5c; 33 | 34 | $primary-light: #ffffff; 35 | $secondary-light: #d1d1d1; 36 | $tertiary-light: #adadad; 37 | 38 | $banner-height: 5vh; 39 | $banner-background: #25282e; 40 | 41 | // Animations 42 | @keyframes button-gradient { 43 | 0% { 44 | background-position: 0% 50%; 45 | } 46 | 50% { 47 | background-position: 100% 50%; 48 | } 49 | 100% { 50 | background-position: 0% 50%; 51 | } 52 | } 53 | 54 | // Classes and Overrides 55 | html { 56 | font-size: 14px; 57 | } 58 | 59 | .h-inherit { 60 | height: inherit; 61 | } 62 | 63 | .no-scrollbar { 64 | scrollbar-width: none; 65 | 66 | &::-webkit-scrollbar { 67 | display: none; 68 | } 69 | } 70 | 71 | .width-fit { 72 | width: fit-content; 73 | } 74 | 75 | .w-100 { 76 | width: 100%; 77 | } 78 | 79 | button { 80 | background: none; 81 | border: none; 82 | padding: unset; 83 | } 84 | 85 | .button-primary { 86 | @extend %button-generic; 87 | background: $button-gradient; 88 | cursor: pointer; 89 | background-size: 200% 200%; 90 | color: white; 91 | 92 | &:hover { 93 | animation: button-gradient 3s ease infinite; 94 | } 95 | } 96 | 97 | .flex-col { 98 | flex-direction: column; 99 | } 100 | 101 | .flex-1 { 102 | flex: 1; 103 | } 104 | 105 | .pulse { 106 | display: flex; 107 | align-items: center; 108 | justify-content: center; 109 | height: 1px; 110 | width: 1px; 111 | border-radius: 50%; 112 | position: relative; 113 | border: none; 114 | box-shadow: 0 0 0 0 #fff; 115 | 116 | &:hover { 117 | animation: pulse 1s infinite cubic-bezier(0.2, 0, 0, 1); 118 | } 119 | 120 | @keyframes pulse { 121 | to { 122 | box-shadow: 0 0 0 30px rgba(255, 255, 255, 0); 123 | } 124 | } 125 | } 126 | 127 | .row-link-container { 128 | position: relative; 129 | 130 | .row-link { 131 | position: absolute; 132 | height: 100%; 133 | width: 100%; 134 | top: 0; 135 | left: 0; 136 | } 137 | } 138 | 139 | .ide-container { 140 | height: 100vh; 141 | width: 100%; 142 | overflow: hidden; 143 | position: relative; 144 | display: flex; 145 | flex-direction: column; 146 | 147 | &--dark { 148 | background: $primary-dark; 149 | color: $primary-light; 150 | } 151 | 152 | .navbar-top { 153 | border-bottom: solid 1px $tertiary-dark; 154 | height: 60px; 155 | padding: 0 30px; 156 | 157 | .cm-logo { 158 | height: 20px; 159 | } 160 | 161 | &__option { 162 | font-size: 1.142rem; 163 | color: $primary-light; 164 | padding: 0 1rem; 165 | cursor: pointer; 166 | text-decoration: none; 167 | display: flex; 168 | align-items: center; 169 | 170 | &__icon { 171 | height: 18px; 172 | } 173 | 174 | &:hover { 175 | color: $accent-color-green; 176 | } 177 | 178 | &:first-child:focus-visible &:not(:first-child) { 179 | display: none; 180 | } 181 | } 182 | 183 | .save-button-container { 184 | position: relative; 185 | padding: 5px 1rem; 186 | border-radius: 50px; 187 | border: 1px solid $tertiary-dark; 188 | 189 | input { 190 | appearance: none; 191 | background: transparent; 192 | border: unset; 193 | color: $tertiary-light; 194 | transition: all 0.5s; 195 | width: 175px; 196 | 197 | &:focus-visible { 198 | outline: unset; 199 | color: $primary-light; 200 | width: 300px; 201 | } 202 | 203 | &::placeholder { 204 | opacity: 1; 205 | color: $tertiary-light; 206 | } 207 | } 208 | 209 | button { 210 | color: $primary-light; 211 | cursor: pointer; 212 | 213 | &:focus, 214 | &:hover { 215 | color: $accent-color-green; 216 | } 217 | } 218 | } 219 | 220 | select { 221 | -webkit-appearance: none; 222 | -moz-appearance: none; 223 | -ms-appearance: none; 224 | appearance: none; 225 | outline: 0; 226 | box-shadow: none; 227 | background: $primary-dark; 228 | background-image: none; 229 | color: #fff; 230 | cursor: pointer; 231 | 232 | &::-ms-expand { 233 | display: none; 234 | } 235 | 236 | option { 237 | background: $primary-dark; 238 | padding: 5px 0; 239 | } 240 | } 241 | 242 | .language-selector { 243 | border: 1px solid $accent-color-green; 244 | border-radius: 5px; 245 | padding: 5px 10px; 246 | margin-right: 30px; 247 | transition: all 0.25s; 248 | 249 | &:hover, 250 | &:focus { 251 | border: 1px solid $accent-color-purple; 252 | background: $tertiary-dark; 253 | } 254 | 255 | @media screen and (max-width: 1199px) { 256 | margin-right: 20px; 257 | } 258 | } 259 | 260 | .fontsize-selector { 261 | @extend .language-selector; 262 | font-weight: 700; 263 | 264 | option { 265 | padding: 5px 10px; 266 | } 267 | } 268 | 269 | .logged-in-user-menu { 270 | position: relative; 271 | 272 | .logged-in-user-box { 273 | font-size: 1.142rem; 274 | font-weight: 600; 275 | background: $overlay-dark; 276 | color: $primary-light; 277 | border-radius: 5px; 278 | padding: 5px 20px; 279 | cursor: pointer; 280 | } 281 | 282 | .floating-menu { 283 | position: absolute; 284 | width: 100%; 285 | bottom: -6rem; 286 | left: 0; 287 | background: $overlay-dark; 288 | border-radius: 5px; 289 | padding: 10px 15px; 290 | font-size: 1rem; 291 | z-index: 10000; 292 | color: $primary-light; 293 | 294 | &--hidden { 295 | display: none; 296 | } 297 | 298 | a, 299 | button { 300 | cursor: pointer; 301 | color: $primary-light; 302 | } 303 | 304 | &__option { 305 | display: flex; 306 | align-items: center; 307 | 308 | &:hover { 309 | color: $accent-color-green; 310 | } 311 | } 312 | } 313 | } 314 | 315 | @media screen and (max-width: 1199px) { 316 | .save-button-container { 317 | input { 318 | width: 125px; 319 | 320 | &:focus-visible { 321 | width: 200px; 322 | } 323 | } 324 | } 325 | } 326 | 327 | @media screen and (max-width: 991px) { 328 | padding: 0 20px; 329 | 330 | .cm-logo { 331 | height: 15px; 332 | } 333 | 334 | &__option { 335 | font-size: 0.857rem; 336 | padding: 0.5rem; 337 | 338 | &__icon { 339 | height: 15px; 340 | } 341 | } 342 | 343 | .language-selector { 344 | font-size: 0.857rem; 345 | } 346 | 347 | .logged-in-user-menu { 348 | .logged-in-user-box { 349 | font-size: 0.857rem; 350 | } 351 | 352 | .floating-menu { 353 | font-size: 0.7857rem; 354 | } 355 | } 356 | 357 | .save-button-container { 358 | input { 359 | width: 75px; 360 | 361 | &:focus-visible { 362 | width: 125px; 363 | } 364 | } 365 | } 366 | } 367 | } 368 | 369 | .run-button__container { 370 | position: fixed; 371 | bottom: calc(1.5rem + 60px); 372 | right: calc(3rem + 60px); 373 | transition: all 0.75s; 374 | 375 | .run-button { 376 | display: flex; 377 | align-items: center; 378 | justify-content: center; 379 | height: 60px; 380 | width: 60px; 381 | border-radius: 50px; 382 | cursor: pointer; 383 | padding: 20px; 384 | position: absolute; 385 | top: 0; 386 | left: 0; 387 | transition: all 0.75s; 388 | 389 | img { 390 | width: 100%; 391 | margin-right: -5px; 392 | } 393 | 394 | &--run-code { 395 | background: $button-gradient; 396 | border: solid 1px $button-gradient; 397 | background-size: 200% 200%; 398 | 399 | &:hover { 400 | animation: button-gradient 3s infinite; 401 | } 402 | } 403 | 404 | &--success { 405 | background: $accent-color-green; 406 | border: solid 1px $accent-color-green; 407 | } 408 | 409 | &--error { 410 | background: #e6bf12; 411 | border: solid 1px #e6bf12; 412 | } 413 | } 414 | 415 | .ring-animation { 416 | position: absolute; 417 | visibility: visible; 418 | opacity: 1; 419 | transition: all 0.25s; 420 | 421 | &--small { 422 | top: -5px; 423 | left: -5px; 424 | } 425 | 426 | &--large { 427 | top: -12.5px; 428 | left: -12.5px; 429 | display: none; 430 | } 431 | 432 | circle { 433 | animation: rotate 5s linear infinite; 434 | fill: transparent; 435 | stroke: $accent-color-green; 436 | stroke-width: 4; 437 | stroke-dasharray: 250; 438 | stroke-dashoffset: 1000; 439 | } 440 | 441 | @keyframes rotate { 442 | to { 443 | stroke-dashoffset: 0; 444 | } 445 | } 446 | } 447 | 448 | @media screen and (max-width: 991px) { 449 | right: calc(2rem + 60px); 450 | } 451 | } 452 | 453 | .main-container { 454 | height: calc(100vh - 60px); 455 | position: relative; 456 | flex: 1; 457 | 458 | .ide-section { 459 | flex: 1; 460 | width: 60%; 461 | max-width: calc(100vw - 30px); 462 | } 463 | 464 | .io-section { 465 | border-left: solid 1px $tertiary-dark; 466 | width: 40%; 467 | max-width: 500px; 468 | position: relative; 469 | transition: all 0.75s; 470 | 471 | &--hidden { 472 | width: 30px !important; 473 | overflow: hidden; 474 | 475 | * { 476 | visibility: hidden; 477 | } 478 | } 479 | 480 | &--hidden ~ .run-button__container { 481 | bottom: calc(3rem + 60px); 482 | 483 | .run-button { 484 | transform: scale(125%); 485 | } 486 | 487 | .ring-animation { 488 | &--large { 489 | display: block; 490 | } 491 | 492 | &--small { 493 | display: none; 494 | } 495 | } 496 | } 497 | 498 | &--hidden ~ .ide-section { 499 | width: calc(100vw - 30px); 500 | } 501 | 502 | &--hidden ~ .open-io-button { 503 | background: $accent-color-green; 504 | right: calc(3rem - 10px); 505 | 506 | .open-io-button__icon { 507 | transform: rotate(180deg); 508 | } 509 | } 510 | 511 | .saved-list-section { 512 | padding: 30px; 513 | position: relative; 514 | 515 | .table-container { 516 | max-height: 55%; 517 | overflow: auto; 518 | } 519 | 520 | table { 521 | width: 100%; 522 | table-layout: fixed; 523 | 524 | .main-key { 525 | width: 40%; 526 | } 527 | 528 | .sub-key { 529 | width: 30%; 530 | } 531 | 532 | tr { 533 | td { 534 | white-space: nowrap; 535 | overflow: hidden; 536 | text-overflow: ellipsis; 537 | } 538 | 539 | &:hover { 540 | td { 541 | color: $accent-color-green; 542 | } 543 | } 544 | } 545 | } 546 | 547 | &__navigation { 548 | position: absolute; 549 | bottom: 30px; 550 | left: 30px; 551 | font-size: 1rem; 552 | 553 | .savelist-pagination-buttons { 554 | font-weight: 600; 555 | 556 | &:hover { 557 | color: $accent-color-green; 558 | cursor: pointer; 559 | } 560 | } 561 | } 562 | 563 | @media screen and (max-width: 991px) { 564 | padding: 20px; 565 | 566 | &__navigation { 567 | font-size: 0.785rem; 568 | } 569 | 570 | table { 571 | thead { 572 | font-size: 0.857rem; 573 | } 574 | 575 | tbody { 576 | font-size: 0.785rem; 577 | } 578 | 579 | .main-key { 580 | width: 35%; 581 | } 582 | 583 | .sub-key { 584 | width: 32.5%; 585 | } 586 | } 587 | } 588 | } 589 | 590 | .io-header { 591 | font-size: 1.142rem; 592 | font-weight: 700; 593 | } 594 | 595 | @media screen and (max-width: 991px) { 596 | &--hidden ~ .open-io-button { 597 | right: calc(2rem - 10px); 598 | } 599 | 600 | .io-header { 601 | font-size: 1rem; 602 | } 603 | } 604 | } 605 | 606 | .io-box { 607 | max-height: 100vh; 608 | textarea { 609 | background: $primary-dark; 610 | width: 100%; 611 | height: 80%; 612 | border: none; 613 | resize: none; 614 | appearance: none; 615 | color: $primary-light; 616 | transition: padding 0.25s; 617 | 618 | &:focus { 619 | border: solid 1px $tertiary-dark !important; 620 | outline: 0 none !important; 621 | box-shadow: 0 0 5px rgba($color: $accent-color-green, $alpha: 1) inset !important; 622 | border-radius: 5px; 623 | padding: 20px; 624 | } 625 | } 626 | 627 | .input-box { 628 | padding: 30px; 629 | border-bottom: solid 1px $tertiary-dark; 630 | height: 50%; 631 | overflow: auto; 632 | } 633 | 634 | .output-box { 635 | padding: 30px; 636 | background: $primary-dark; 637 | height: 50%; 638 | overflow: auto; 639 | } 640 | 641 | pre { 642 | color: $primary-light; 643 | } 644 | 645 | @media screen and (max-width: 991px) { 646 | .input-box { 647 | padding: 20px; 648 | } 649 | 650 | .output-box { 651 | padding: 20px; 652 | } 653 | } 654 | } 655 | 656 | .io-navigation { 657 | display: flex; 658 | align-items: center; 659 | font-size: 1.25rem; 660 | background: $tertiary-dark; 661 | width: 100%; 662 | 663 | & .tab { 664 | cursor: pointer; 665 | display: flex; 666 | align-items: center; 667 | padding: 15px 25px; 668 | 669 | &:hover, 670 | &.active { 671 | background: $primary-dark; 672 | } 673 | } 674 | } 675 | 676 | .open-io-button { 677 | cursor: pointer; 678 | font-weight: bold; 679 | position: absolute; 680 | top: 20px; 681 | right: 3rem; 682 | height: 40px; 683 | width: 40px; 684 | border-radius: 50%; 685 | display: flex; 686 | align-items: center; 687 | justify-content: center; 688 | z-index: 100; 689 | border: 1px solid $accent-color-green; 690 | transition: all 0.75s; 691 | 692 | &__icon { 693 | transition: all 0.75s; 694 | } 695 | 696 | @media screen and (max-width: 991px) { 697 | right: 2rem; 698 | top: 10px; 699 | } 700 | } 701 | } 702 | 703 | .overlay { 704 | height: 100vh; 705 | width: 100vw; 706 | position: fixed; 707 | top: 0; 708 | left: 0; 709 | z-index: 500; 710 | background: rgba($color: #363a43, $alpha: 0.6); 711 | display: flex; 712 | align-items: center; 713 | justify-content: center; 714 | 715 | &--hidden { 716 | display: none; 717 | } 718 | 719 | &__modal { 720 | height: fit-content; 721 | max-height: 80%; 722 | overflow: auto; 723 | width: 90vw; 724 | max-width: 500px; 725 | background: $primary-dark; 726 | border-radius: 10px; 727 | padding: 50px 30px; 728 | display: flex; 729 | justify-content: center; 730 | align-items: center; 731 | position: relative; 732 | 733 | &__close-button { 734 | position: absolute; 735 | top: 30px; 736 | right: 30px; 737 | cursor: pointer; 738 | } 739 | 740 | .w-100 { 741 | text-align: center; 742 | } 743 | 744 | &__title { 745 | color: $primary-light; 746 | font-weight: 700; 747 | font-size: 1.285rem; 748 | } 749 | 750 | &__divider { 751 | background-color: $tertiary-dark; 752 | margin: 30px 0; 753 | height: 1px; 754 | width: 100%; 755 | } 756 | } 757 | } 758 | 759 | .login-card { 760 | padding: 0.75rem 1.5rem; 761 | font-weight: 700; 762 | font-size: 1.142rem; 763 | width: 60%; 764 | min-width: fit-content; 765 | color: $primary-light; 766 | border-radius: 5px; 767 | margin: 0 auto; 768 | cursor: pointer; 769 | transition: all 0.5s ease; 770 | 771 | &--google { 772 | background: $primary-light; 773 | color: $primary-dark; 774 | border: 1px solid $primary-light; 775 | 776 | &:hover { 777 | color: $primary-light; 778 | } 779 | } 780 | 781 | &--github { 782 | background: $secondary-color-green; 783 | border: 1px solid $secondary-color-green; 784 | } 785 | 786 | &--linkedin { 787 | background: #0077b5; 788 | border: 1px solid #0077b5; 789 | } 790 | 791 | &:hover { 792 | background: transparent; 793 | } 794 | } 795 | } 796 | 797 | .top-banner { 798 | // position: absolute; 799 | padding: 10px 30px; 800 | height: $banner-height; 801 | display: flex; 802 | width: 100%; 803 | align-items: center; 804 | justify-content: space-between; 805 | background-color: $banner-background; 806 | color: $primary-light; 807 | 808 | a { 809 | text-decoration: none; 810 | color: inherit; 811 | } 812 | } 813 | 814 | .top-banner .top-banner--cross { 815 | color: $primary-light; 816 | font-size: 20px; 817 | cursor: pointer; 818 | } 819 | 820 | .top-banner .banner--green-text { 821 | color: $accent-color-green; 822 | } 823 | 824 | .input-container { 825 | position: relative; 826 | 827 | .transparent-input { 828 | appearance: none; 829 | background: transparent; 830 | padding: 10px 40px 10px 20px; 831 | border-radius: 50px; 832 | border: 1px solid $tertiary-dark; 833 | color: $tertiary-light; 834 | 835 | &:focus-visible { 836 | outline: unset; 837 | } 838 | 839 | &::placeholder { 840 | opacity: 1; 841 | color: $tertiary-light; 842 | } 843 | 844 | // &::after { 845 | // content: url(https://minio.codingminutes.com/assets/search.svg); 846 | // position: absolute; 847 | // right: 30px; 848 | // top: 0; 849 | // } 850 | } 851 | 852 | &__icon { 853 | position: absolute; 854 | right: 15px; 855 | top: 7.5px; 856 | } 857 | } 858 | 859 | .savelist-pagination-buttons { 860 | color: white; 861 | } 862 | 863 | .toast { 864 | position: absolute; 865 | bottom: -100px; 866 | left: 45vw; 867 | 868 | min-height: 30px; 869 | min-width: 10vw; 870 | 871 | border-radius: 10px; 872 | background: $button-gradient; 873 | background-size: 200% 200%; 874 | color: white; 875 | padding: 10px 20px; 876 | text-align: center; 877 | z-index: 1000; 878 | 879 | transition: all 0.5s ease-in-out; 880 | 881 | &:hover { 882 | animation: button-gradient 3s ease infinite; 883 | } 884 | 885 | &.active { 886 | bottom: 20px; 887 | } 888 | } 889 | 890 | // REUSE LATER 891 | 892 | // .language-selector { 893 | // padding: 10px 20px; 894 | // border: 1px solid $accent-color-green; 895 | // font-weight: 500; 896 | // border-radius: 5px; 897 | // cursor: pointer; 898 | // position: absolute; 899 | // right: 5rem; 900 | // top: calc(1.5rem + 2px); 901 | // min-width: 150px; 902 | 903 | // &__active { 904 | // text-align: center; 905 | // font-weight: bold; 906 | // } 907 | 908 | // &__content { 909 | // height: 0; 910 | // transition: all 0.25s; 911 | 912 | // &__item { 913 | // display: none; 914 | // } 915 | // } 916 | 917 | // &:hover, 918 | // &:active, 919 | // &:focus-within, 920 | // &:focus, 921 | // &:focus-visible { 922 | // background: $tertiary-dark; 923 | // border: solid 1px $accent-color-pink; 924 | // padding: 10px 20px; 925 | 926 | // .language-selector__content { 927 | // height: 150px; 928 | // padding-top: 15px; 929 | // overflow: auto; 930 | 931 | // scrollbar-width: none; 932 | 933 | // &::-webkit-scrollbar { 934 | // display: none; 935 | // } 936 | 937 | // &__item { 938 | // display: block; 939 | 940 | // &:hover { 941 | // color: $accent-color-green; 942 | // } 943 | // } 944 | // } 945 | // } 946 | // } 947 | --------------------------------------------------------------------------------